/* 
 * File:   Client.cpp
 * Author: sgk
 *
 * Created on March 21, 2010, 11:06 AM
 */

#include <stdlib.h>
#include <iostream>

#include <netinet/in.h> // for htons etc
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netdb.h>

#include "Utils.h"
#include "Message.h"

#include <sys/time.h>

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>

using namespace std;

volatile int sigcount = 1; // for monitor timeout

class Client {
public:
	Client(int argc, char** argv);
	~Client();
	int showMenu();
	int selectMenu(int input);

	// client stubs
	int openAccount_stub(string name, string password, int currencyType, float balance);
	int closeAccount_stub(string name, string password, int accountNum);
	int depositWithdraw_stub(string name, string password, int accountNum, int currencyType, float amount);
	int checkBalance(string name, string password, int accountNum);
	int transfer_stub(string name, string password, int accountNum, int currencyType, float amount, int accountNumToTransfer);
	int monitorUpdates_stub(int interval);

	string send(char*);
	string receive() throw (int);
	string monitor() throw (int);

	// static methods for getting and checking user inputs
	static string getName() throw (string);
	static string getPassword() throw (string);
	static int getAccountNum() throw (string);
	static int getCurrencyType() throw (string);
	static float getAmount() throw (string);
	static float getInitBalance() throw (string);
	static int getInterval() throw (string);
        static int getAccountNumToTransfer() throw (string);


private:
	int clientSocket;
	struct sockaddr_in serverAddress;
	struct sockaddr_in clientAddress;
	struct hostent *serverHost;
	struct hostent *clientHost;

	int clientPort;
	int serverPort;

	string serverIP;
	int requestId;
	double packetLossRate;
	long timeout;

};

void monitorTimeout(int sig);
int checkTimeout(int s, long seconds);

Client::Client(int argc, char** argv) {

	// default values
	serverIP = "127.0.0.1";
	clientPort = 9866;
	serverPort = 9876;

	requestId = 0;
	packetLossRate = 0;
	timeout = 5;

	// get commandline arguments
	//	-a 127.0.0.1 -p 9876 -c 9873 -r 0
	for (int i = 0; i < argc; i++) {

		// get server address
		if (strcmp(argv[i], "-a") == 0) {
			serverIP = argv[i+1];
		}

		// get server port
		if (strcmp(argv[i], "-p") == 0) {
			if(atoi(argv[i+1]) != 0){
				serverPort = atoi(argv[i+1]);
			}
		}

		// get packet loss rate
		if (strcmp(argv[i], "-r") == 0) {
			if(atof(argv[i+1]) != 0){
				packetLossRate = atof(argv[i+1]);
			}
			
		}

		// get timeout
		if (strcmp(argv[i], "-t") == 0) {
			if(atoi(argv[i+1]) != 0){
				timeout = atoi(argv[i+1]);
			}
		}

		// get client port
		if (strcmp(argv[i], "-c") == 0) {
			if(atoi(argv[i+1]) != 0){
				clientPort = atoi(argv[i+1]);
			}
		}
	}

	this->serverHost = (struct hostent *) gethostbyname((char *) serverIP.c_str());
	this->clientHost = (struct hostent *) gethostbyname((char *) serverIP.c_str());

	// prep server address
	this->serverAddress.sin_family = AF_INET;
	this->serverAddress.sin_port = htons(serverPort);
	this->serverAddress.sin_addr = *((struct in_addr *) this->serverHost->h_addr);
	bzero(&(this->serverAddress.sin_zero), 8);

	// prep client address
	this->clientAddress.sin_family = AF_INET;
	this->clientAddress.sin_port = htons(clientPort);
	this->clientAddress.sin_addr = *((struct in_addr *) this->clientHost->h_addr);
	bzero(&(this->clientAddress.sin_zero), 8);

	// print server settings
	cout << "[CLIENT SETTINGS]" << endl;
	cout << "-----------------" << endl;
	cout << "Client Port: " << clientPort << endl;
	cout << "Server Address: " << serverIP << endl;
	cout << "Server Port: " << serverPort << endl;
	cout << "Packet Loss Rate: " << packetLossRate * 100 << "%  Timeout: " << timeout << "secs" << endl;
	cout << endl;

}

Client::~Client() {

}

int Client::showMenu() {

	int output = -1;
	int input = -1;

	char inputTemp[256];

	// get user input
	while (output != 0) {

		// print menu
		cout << endl;
		cout << "     Welcome to XYZ Banking Menu    " << endl;
		cout << "------------------------------------" << endl;
		cout << "   Available services:" << endl;
		cout << "1. Open Account   2. Close Account" << endl;
		cout << "3. Deposit        4. Withdraw" << endl;
		cout << "5. Check Balance  6. Transfer" << endl;
		cout << "7. Monitor Updates" << endl;
		cout << "------------------------------------" << endl;
		cout << "Enter your choice (1-7) or any key to exit " << endl;
		cout << "> ";

		// read input from keyboard
		cin.getline(inputTemp, 256);
		input = strtol(inputTemp, NULL, 0);

		// call appropriate client stub based on input
		output = this->selectMenu(input);

	}
	return (EXIT_SUCCESS);
}

int Client::selectMenu(int input) {

	// simple if/else to call client stub based on input
	if (input == 0) {

		// exit program
		cout << endl;
		cout << "Thank you for banking with us" << endl;
		cout << endl;
		return 0;

	} else if (input == 1) {
		cout << endl;
		cout << "-----------------" << endl;
		cout << " Open an account " << endl;
		cout << "-----------------" << endl;

		// get necessary inputs from user
		try {

			// this is ALL of the input methods. use accordingly
			string name = Client::getName();
			string password = Client::getPassword();
			int currencyType = Client::getCurrencyType();
			float initbal = Client::getInitBalance();
			
			// call stub
			cout << endl;
			cout << "[CLIENT] Opening new account..." << endl;
			this->openAccount_stub(name, password, currencyType, initbal);

		} catch (string e) {

			cout << e << endl;
			return -1;

		}

	} else if (input == 2) {
		cout << endl;
		cout << "------------------" << endl;
		cout << " Close an account " << endl;
		cout << "------------------" << endl;

                // get necessary inputs from user
		try {

			string name = Client::getName();
			string password = Client::getPassword();
			int accountNum = Client::getAccountNum();

			// call stub
			cout << endl;
			cout << "[CLIENT] Closing account..." << endl;
			this->closeAccount_stub(name, password, accountNum);

		} catch (string e) {

			cout << e << endl;
			return -1;

		}


	} else if (input == 3 || input == 4) {

		cout << endl;
		if (input == 3) {
			cout << endl;
			cout << "-----------------" << endl;
			cout << " Account deposit " << endl;
			cout << "-----------------" << endl;
		}

		if (input == 4) {
			cout << endl;
			cout << "------------------" << endl;
			cout << " Account withdraw " << endl;
			cout << "------------------" << endl;
		}

		// get necessary inputs from user
		try {

			string name = Client::getName();
			string password = Client::getPassword();
			int accountNum = Client::getAccountNum();
			int currencyType = Client::getCurrencyType();
			float amount = Client::getAmount();

			if (input == 4) {
				amount = amount * (-1);
			}

			if (input == 3) {
				cout << endl << "[CLIENT] Processing deposit..." << endl;
			}

			if (input == 4) {
				cout << endl << "[CLIENT] Processing withdrawal..." << endl;
			}
			// call withdraw/deposit stub
			this->depositWithdraw_stub(name, password, accountNum, currencyType, amount);

		} catch (string e) {

			cout << e << endl;
			return -1;

		}


	} else if (input == 5) {
		cout << endl;
		cout << "---------------" << endl;
		cout << " Check balance " << endl;
		cout << "---------------" << endl;

        // get necessary inputs from user
		try {

			string name = Client::getName();
			string password = Client::getPassword();
			int accountNum = Client::getAccountNum();

			// call stub
			cout << endl;
			cout << "[CLIENT] Retrieving account balance..." << endl;
            this->checkBalance(name, password, accountNum);

		} catch (string e) {

			cout << e << endl;
			return -1;

		}
	} else if (input == 6) {
		cout << endl;
		cout << "-----------------------------" << endl;
		cout << " Transfer to another account " << endl;
		cout << "----=------------------------" << endl;

                // get necessary inputs from user
		try {

			string name = Client::getName();
			string password = Client::getPassword();
			int accountNum = Client::getAccountNum();
			int currencyType = Client::getCurrencyType();
			float amount = Client::getAmount();
            int accountNumToTransfer = Client::getAccountNumToTransfer();

			// call stub
			cout << endl;
			cout << "[CLIENT] Transfering to another account..." << endl;
                        this->transfer_stub(name, password, accountNum, currencyType, amount, accountNumToTransfer);

		} catch (string e) {

			cout << e << endl;
			return -1;

		}

	} else if (input == 7) {

		int interval = -1;

		cout << endl;
		cout << "-----------------" << endl;
		cout << " Monitor Updates " << endl;
		cout << "-----------------" << endl;

		// get necessary inputs from user
		try {

			// get desired monitor interval
			interval = Client::getInterval();

		} catch (string e) {

			cout << e << endl;
			return -1;

		}

		// call stub
		this->monitorUpdates_stub(interval);
	} else {

		// invalid input
		cout << "[ERROR] Invalid input" << endl;
	}

	return -1;
}

int Client::openAccount_stub(string name, string password, int currencyType, float balance) {
    
    int methodId = 1;

	string response;

	char sendData[1024] = {0};

	// prepare marshalled message for send
	Message::pack(sendData, this->requestId, methodId, name, password, currencyType, balance);

	// increase request id
	this->requestId++;

	// send message and receive response from server
	response = this->send(sendData);

	// display response from server
	cout << "[SERVER] " << response << endl;

}


int Client::closeAccount_stub(string name, string password, int accountNum) {

    int methodId = 2;

	string response;

	char sendData[1024] = {0};

	// prepare marshalled message for send
	Message::pack(sendData, this->requestId, methodId, name, password, accountNum);

	// increase request id
	this->requestId++;

	// send message and receive response from server
	response = this->send(sendData);

	// display response from server
	cout << "[SERVER] " << response << endl;
}

int Client::depositWithdraw_stub(string name, string password, int accountNum, int currencyType, float amount) {

	int methodId = 3;

	string response;

	char sendData[1024] = {0};

	// prepare marshalled message for send
	Message::pack(sendData, this->requestId, methodId, name, password, accountNum, currencyType, amount);

	// increase request id
	this->requestId++;

	// send message and receive response from server
	response = this->send(sendData);

	// display response from server
	cout << "[SERVER] " << response << endl;

}

int Client::checkBalance(string name, string password, int accountNum) {

    int methodId = 5;

	string response;

	char sendData[1024] = {0};

	// prepare marshalled message for send
	Message::pack(sendData, this->requestId, methodId, name, password, accountNum);

	// increase request id
	this->requestId++;

	// send message and receive response from server
	response = this->send(sendData);

	// display response from server
	cout << "[SERVER] " << response << endl;

}

int Client::transfer_stub(string name, string password, int accountNum, int currencyType, float amount, int accountNumToTransfer) {

    int methodId = 6;

	string response;

	char sendData[1024] = {0};

	// prepare marshalled message for send
	Message::pack(sendData, this->requestId, methodId, name, password, accountNum, currencyType, amount, accountNumToTransfer);

	// increase request id
	this->requestId++;

	// send message and receive response from server
	response = this->send(sendData);

	// display response from server
	cout << "[SERVER] " << response << endl;
}

int Client::monitorUpdates_stub(int interval) {

	int methodId = 7;

	string response;

	char sendData[1024] = {0};

	/////////////////////////////////////////
	// send registration message to server //
	/////////////////////////////////////////
	// prepare marshalled message for send
	Message::pack(sendData, this->requestId, methodId, interval);

	// increase request id
	this->requestId++;

	cout << "[CLIENT] Registering client for updates monitoring..." << endl;

	// send message and receive response from server
	response = this->send(sendData);

	// display response from server
	if (response.compare("OK") == 0) {
		cout << "[CLIENT] Client successfully registered." << endl;
	} else {
		cout << "[ERROR] Error in registration (" << response << ")" << endl;
		return -1;
	}

	cout << "[CLIENT] Monitoring updates..." << endl;

	////////////////////////
	// monitoring updates //
	////////////////////////

	bool monitoring = true;
	sigcount = 0;

	// preparing alarm params
	struct sigaction sact;
	sigemptyset(&sact.sa_mask);
	sact.sa_flags = 0;
	sact.sa_handler = monitorTimeout;
	sigaction(SIGALRM, &sact, NULL);

	// start timer
	alarm(interval); /* timer will pop in interval seconds */

	// open socket for monitoring
	// create udp socket
	if ((this->clientSocket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}

	// bind socket to address
	bind(this->clientSocket, (struct sockaddr *) & this->clientAddress, sizeof (struct sockaddr));

	while (monitoring) {

		// start listening for incoming
		response = this->monitor();

		// if alarm was triggered
		if (sigcount == 1) {
			monitoring = false;
		} else {
			// display received data from server
			cout << "[SERVER] " << response << endl;
		}

	}

	// monitoring period ended
	cout << "[CLIENT] Monitoring period ended" << endl;

	// close monitoring socket
	close(this->clientSocket);

}

void monitorTimeout(int sig) {
	cout << "[CLIENT] Connection successfully closed" << endl;
	sigcount = 1;
}

string Client::send(char* data) {

	string response = "none";

	bool isPacketLost = false;
	bool timeout = false;

	// create udp socket
	if ((this->clientSocket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}

	// bind socket to address
	bind(this->clientSocket, (struct sockaddr *) & this->clientAddress, sizeof (struct sockaddr));

	do {

		// do a coin flip to simulate loss of request message
		isPacketLost = Utils::isDroppedPacket(this->packetLossRate);

		// send data packet if its not supposed to be lost
		if (!isPacketLost) {

			cout << "[NETWORK] Packet sent" << endl;

			// send message
			sendto(this->clientSocket, data, 1024, 0,
					(struct sockaddr *) & this->serverAddress, sizeof (struct sockaddr));

		} else {
			cout << "[NETWORK] Packet dropped" << endl;
		}

		// wait for response from server
		try {

			response = this->receive();
			timeout = false;

		} catch (int e) {
			if (e == -1) {
				cout << "[NETWORK] Timeout!" << endl;
				timeout = true;
			}
		}

	} while (timeout);

	// close socket
	close(this->clientSocket);

	return response;

}

int checkTimeout(int s, long seconds) {
	unsigned long read_mask;
	struct timeval timeout;
	int n;

	timeout.tv_sec = seconds;
	timeout.tv_usec = 0; //milliseconds * 1000;

	read_mask = (1 << s);

	if ((n = select(32, (fd_set*) & read_mask, 0, 0, &timeout)) < 0) {
		perror("Select fail:");
	} else {
		//printf("n=%d", n);
	}
	return n;
}

string Client::receive() throw (int) {
	string response;
	char recvData[1024];
	int numBytesRead;
	socklen_t addressLength = sizeof (struct sockaddr);

	// receive response from server, with timeout
	int n = checkTimeout(this->clientSocket, this->timeout);
	if (n == 0) {
		// timeout
		throw -1;
	}
	numBytesRead = recvfrom(this->clientSocket, recvData, 1024, 0,
			(struct sockaddr *) & this->serverAddress, &addressLength);


	// make char array null-terminated
	recvData[numBytesRead] = '\0';

	fflush(stdout);

	// convert char array to string
	response.assign(recvData);

	return response;

}

string Client::monitor() throw (int) {

	string response;
	char recvData[1024];
	int numBytesRead;
	socklen_t addressLength = sizeof (struct sockaddr);

	numBytesRead = recvfrom(this->clientSocket, recvData, 1024, 0,
			(struct sockaddr *) & this->serverAddress, &addressLength);

	// make char array null-terminated
	recvData[numBytesRead] = '\0';

	fflush(stdout);

	// convert char array to string
	response.assign(recvData);

	return response;

}

string Client::getName() throw (string) {
	string name = "";

	cout << "Enter name > ";

	getline(cin, name);

	if (name == "") {
		throw string("[ERROR] Name cannot be blank");
	}

	return name;
}

string Client::getPassword() throw (string) {
	string password = "";

	cout << "Enter password (4 chars) > ";

	getline(cin, password);

	// do appropriate checks
	if (password.length() != 4) {
		throw string("[ERROR] Password length must be 4 characters.");
	}

	return password;

}

int Client::getAccountNum() throw (string) {
	int value = -1;
	char temp[256];

	cout << "Enter account # > ";

	cin.getline(temp, 256);
	value = strtol(temp, NULL, 0);

	return value;
}

int Client::getCurrencyType() throw (string) {
	int value = -1;
	char temp[256];

	cout << "Enter currency type (0 for SGD or 1 for USD) > ";

	cin.getline(temp, 256);
	value = strtol(temp, NULL, 0);

	if (value < 0 || value > 1) {
		throw string("[ERROR] Please enter either 0 for SGD or 1 for USD.");
	}

	return value;
}

float Client::getAmount() throw (string) {
	float amount = 0;
	char temp[256];

	cout << "Enter amount > ";

	cin.getline(temp, 256);
	amount = strtof(temp, NULL);

	if (amount <= 0) {
		throw string("[ERROR] Please enter a number larger than 0");
	}

	return amount;
}

float Client::getInitBalance() throw (string) {
	float amount = 0;
	char temp[256];

	cout << "Enter initial balance > ";

	cin.getline(temp, 256);
	amount = strtof(temp, NULL);

	if (amount < 0) {
		throw string("[ERROR] Please enter a number larger than or equal to 0");
	}

	return amount;
}

int Client::getInterval() throw (string) {
	int value = -1;
	char temp[256];

	cout << "Enter desired monitor interval (in seconds) > ";

	cin.getline(temp, 256);
	value = strtol(temp, NULL, 0);

	if (value <= 0) {
		throw string("[ERROR] Please enter a number larger than 0");
	}

	return value;
}

int Client::getAccountNumToTransfer() throw (string) {
	int value = -1;
	char temp[256];

	cout << "Enter account # to transfer > ";

	cin.getline(temp, 256);
	value = strtol(temp, NULL, 0);

	return value;
}

int main(int argc, char** argv) {
	Client client(argc, argv);
	client.showMenu();
}

